package aaa


import io.grpc.ManagedChannel
import io.grpc.ManagedChannelBuilder
import aaa.generated_pb.GreeterGrpcKt.GreeterCoroutineStub

import java.util.Random
import com.fasterxml.jackson.databind.ObjectMapper
import com.google.protobuf.kotlin.DslList
import io.grpc.StatusException
import io.swagger.v3.oas.annotations.Hidden
import io.swagger.v3.oas.annotations.OpenAPIDefinition
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.info.Info
import io.swagger.v3.oas.annotations.media.ExampleObject
import io.swagger.v3.oas.annotations.tags.Tag
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
import java.nio.file.Files
import java.nio.file.Paths
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
import java.text.ParseException
import java.text.SimpleDateFormat
import java.time.Duration
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.Instant
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import java.util.Date
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArraySet
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.event.ApplicationReadyEvent
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Primary
import org.springframework.context.event.EventListener
import org.springframework.core.io.ByteArrayResource
import org.springframework.core.io.Resource
import org.springframework.data.domain.Page
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Sort
import org.springframework.data.jpa.domain.Specification
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.JpaSpecificationExecutor
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.scheduling.annotation.EnableScheduling
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional

import java.util.Calendar
import org.springframework.web.socket.CloseStatus
import org.springframework.web.socket.TextMessage
import org.springframework.web.socket.WebSocketSession
import org.springframework.web.socket.config.annotation.EnableWebSocket
import org.springframework.web.socket.config.annotation.WebSocketConfigurer
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry
import org.springframework.web.socket.handler.TextWebSocketHandler
import io.grpc.StatusRuntimeException
import org.springframework.web.bind.annotation.*
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.ConcurrentLinkedQueue

import aaa.generated_pb.ConnectionStat as pb_ConnectionStat
import aaa.generated_pb.helloRequest as pb_helloRequest
import aaa.generated_pb.empty as pb_empty
import aaa.generated_pb.getProcessInfoRpl as pb_getProcessInfoRpl
import aaa.generated_pb.pswdLogin as pb_pswdLogin
import aaa.generated_pb.GetProcessInfoRpl as pb_GetProcessInfoRpl
import aaa.generated_pb.HelloReply as pb_HelloReply
import aaa.generated_pb.GetBashHistoryRpl as pb_GetBashHistoryRpl
import aaa.generated_pb.GetNICInfoRpl as pb_GetNICInfoRpl
import aaa.generated_pb.InterfaceStat as pb_InterfaceStat
import aaa.generated_pb.setInboundRulesReq as pb_setInboundRulesReq
import aaa.generated_pb.GetUsersRpl as pb_GetUsersRpl
import aaa.generated_pb.GetNetstatsInfoRpl as pb_GetNetstatsInfoRpl
import aaa.generated_pb.getLoginHistoryReq as pb_getLoginHistoryReq
import aaa.generated_pb.GetLoginHistoryRpl as pb_GetLoginHistoryRpl
import aaa.generated_pb.GetResourceInfoRpl as pb_GetResourceInfoRpl
import aaa.generated_pb.OneoffProcessInfo as pb_OneoffProcessInfo
import java.util.stream.Collectors

/**
 * @brief The base class for all controllers
 */
internal open class ControllerBase{

    @Autowired
    lateinit var c : Common //🦜 : Because we need a field.. we use <- 

    // 🦜 : Since spring 4.3, @Autowired is not needed
    // @Autowired
    // var c: Common? = null

    var fmt: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")


    companion object {
        var log: Logger = LoggerFactory.getLogger(ControllerBase::class.java)
        fun old2New(d: Date): LocalDateTime {
            // java 9
            return LocalDateTime.ofInstant(d.toInstant(), ZoneId.systemDefault())
        }

        fun new2Old(l: LocalDateTime): Date {
            return Date.from(l.atZone(ZoneId.systemDefault()).toInstant())
        }

        fun <T, Tk, Tr : JpaRepository<T, Tk>> getPage(repo: Tr, pageNum: Int, pageSize: Int): W<Dt.Page<T>?> {
            if (pageNum < 1 || pageSize < 0) {
                return W(0, null, "不合法的页码或页大小")
            }
            //从数据库中查询
            // --------------------------------------------------
            val page = repo.findAll(PageRequest.of(pageNum - 1, pageSize))
            // return new Dt.Page<T>(page.getContent(),page.getTotalElements());
            return W(0, Dt.Page(page.content, page.totalElements))
        }

        fun checkThingsAndPreparePageable(
            pageNum: Int, pageSize: Int, sortBys: Set<String?>, sortBy0: String?, asc: Boolean?,
            defaultSortBy: String?
        ): Pageable {
            var sortBy = sortBy0 //change to var
            require(!(pageNum < 1 || pageSize < 0)) { "pageNum or pageSize is illegal" }
            // throws IllegalArgumentException

            if (sortBy == null || sortBy.isEmpty()) {
                sortBy = defaultSortBy
            } else require(sortBys.contains(sortBy)) { "sortBy is illegal, it can only be one of $sortBys" }

            // --------------------------------------------------
            // 🦜 : use the powerfull spring-data, so powerful
            var sort = Sort.by(sortBy)
            sort = if (asc != null && !asc) {
                sort.descending()
            } else {
                sort.ascending()
            }

            val pageable: Pageable = PageRequest.of(pageNum - 1, pageSize, sort)
            return pageable
        }
    }
}                               // ControllerBase

internal abstract class ControllerThatSyncs : ControllerBase() {
    var lastSyncTime: LocalDateTime = LocalDateTime.now().minusDays(1) // init to a time in the past
    var cacheValidDuration: Duration = Duration.ofMinutes(20)

    abstract fun sync()
    fun syncIfTooOld() {
        if (lastSyncTime.plus(cacheValidDuration).isBefore(LocalDateTime.now())) {
            log.info("🐢 : cache is too old, syncing")
            this.sync()
            lastSyncTime = LocalDateTime.now()
        }
    }
}

// --------------------------------------------------

@Tag(name = "兼容旧版的接口", description = "🐢 : 旧版的接口们")
@RestController
@RequestMapping("/old")
internal class OldController : ControllerBase() {

    @PostMapping("/machine/updateMachineUsers")
    @Operation(summary = "更新机器用户列表", description = "更新用户列表。这个会叫前置软件")
    fun updateMachineUsers(): Dt.Error {
        /*
          🦜 : Since we have switched to a lazy-eval mode. For backward compatibility, just answer ok here.
         */
        return Common.ok()
    }

    @Autowired
    lateinit var cmdHistoryController: CmdHistoryController

    @GetMapping("/bashHistory/getBashHistoryInfosByPages")
    @Operation(summary = "获取bash历史信息")
    fun getBashHistoryInfosByPages(
        pageNum: Int, pageSize: Int,
        @RequestParam(required = false) startTime: String?,
        @RequestParam(required = false) endTime: String?,
        @RequestParam(required = false) userName: String?,
        @RequestParam(required = false) isIllegal: Boolean?
    ): W<Dt.Page<Dt.OldBashHistoryDao>> {

        if (isIllegal != null && isIllegal == true) {
            // return empty, no illegal records
            return W(0, Dt.Page(ArrayList(), 0L))
        }

        // 🦜 : calls the new api
        val w = cmdHistoryController.get(
            pageNum, pageSize, null,
            userName, null, startTime, endTime,
            null
        )

        val l: MutableList<Dt.OldBashHistoryDao> = ArrayList()
        for (i in w.data.voList.filterNotNull()) {
            val o = Dt.OldBashHistoryDao.oldFromNew(i)
            l.add(o)
        }

        return W(0, Dt.Page(l, w.data.totalCount))
    }

    @Autowired
    lateinit var basicController: BasicController

    @GetMapping("/loginHistory/getLoginHistoryInfosByPages")
    @Operation(summary = "获取登录历史信息")
    fun getLoginHistoryInfosByPages(
        pageNum: Int, pageSize: Int,
        @RequestParam(required = false) startTime: String?,
        @RequestParam(required = false) endTime: String?,
        @RequestParam(required = false) userName: String?
    ): W<Dt.Page<Dt.OldLoginHistory>> {
        // 1. call the new api
        val w = basicController
            .getLoginHistory(pageNum, pageSize, userName, startTime, endTime, "", true)

        // 2. convert the new api's result to the old api's result
        val l: MutableList<Dt.OldLoginHistory> = ArrayList()
        for (i in w.data.voList.filterNotNull()) {
            val o = Dt.OldLoginHistory.oldFromNew(i)
            l.add(o)
        }

        // 3. return
        return W(0, Dt.Page(l, w.data.totalCount))
    }


    @Operation(summary = "获取机器预装软件状态",
               description = "Currently it's mocked"
    )
    @GetMapping("/machine/getPresoftStatus")
    fun getPresoftStatus(): W<Dt.OldPresoftStatus> = W(0, Dt.OldPresoftStatus.next())

    @Operation(summary = "获取机器检查点列表")
    @GetMapping("/machine/getCheckPoints")
    fun getCheckPoints(): W<Dt.OldTrustablePointsVo> = W(0, Dt.OldTrustablePointsVo.mock())

    @GetMapping("/machine/getMachineUsers")
    @Operation(summary = "获取机器用户列表", description = "获取用户列表。新API是`/basic/getUsers`. 🦜 :这个其实每次都会给出所有的users,`pageNum``和`pageSize` query parameters不会被使用。")
    fun getMachineUsers(pageNum: Int, pageSize: Int): W<Dt.Page<Dt.OldMachineUserInfo>> {
        // 1. call the new api
        val w = basicController.getUsers()
        // 2. convert the new api's result to the old api's result
        val l: MutableList<Dt.OldMachineUserInfo> = ArrayList()
        for (i in w.data.voList.filterNotNull()) {
            val o = Dt.OldMachineUserInfo.oldFromNew(i)
            l.add(o)
        }

        // 3. 🦜 : for backward compatibility, we need to provide paging.
        val l2 = pageList(l, pageNum, pageSize)

        // 4. return
        return W(0, Dt.Page(l2, w.data.totalCount))
    }

    @Operation(summary = "获取机器运行详情")
    @GetMapping("/machine/getRunDetail")
    fun getRunDetail(): W<Dt.OldRunDetail?> {
        /*

         🦜 : Got from `uptime -s` and `uptime -p`

         call
         rpc GetBootTime (Empty) returns (HelloReply) {} // 🦜 : Get a string of format "YYYY-MM-DD HH:MM:SS"
         */

        val o = Dt.OldRunDetail()

        try {
            runBlocking{
                val r = c.stub.getBootTime(pb_empty({}))
                o.boot_time = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(r.msg)
            }
        } catch (e: StatusException) {
            log.error(S.RED + "📕️ RPC failed: {}" + S.NOR, e.status)
            return W(1, null, e.status.description ?: "unknown error during grpc getBootTime()")
        } catch (e: ParseException) {
            log.error(S.RED + "📕️ Parse failed: {}" + S.NOR, e.message ?: "unkown error during parsing")
            return W(1, null, e.message!!)
        }

        return W(0, o)
    }


    @EventListener(ApplicationReadyEvent::class)
    fun doSomethingAfterStartup() {
        log.info("🐢 : OldController started, but we do nothing...")
    }

    fun <T> pageList(list: List<T>, pageNum: Int, pageSize: Int): List<T> {
        // check pageNum and pageSize
        require(!(pageNum < 1 || pageSize < 0)) { "pageNum or pageSize is illegal" }

        /*
         [2024-03-01] : 🦜 : We can use the powerful kotlin collection api
         */
        val pages = list.chunked(pageSize)
        if (pageNum > pages.size) {
            return ArrayList() // overflows
        }
        return pages[pageNum - 1]

    }

}                               //OldController


internal interface LoginInfoRepo : JpaRepository<Dt.LoginInfo2, Long>, JpaSpecificationExecutor<Dt.LoginInfo2> {
    fun countByLoginTimeBetween(since: Date, until: Date): Long
}

internal interface ApplicantRepo : JpaRepository<Dt.Applicant, Long>, JpaSpecificationExecutor<Dt.Applicant> {
    fun countByCreateTimeBetween(since: Date, until: Date): Long
}

@Tag(name = "基础管理", description = "🦜: 基础系统管理功能，包括查看用户,登入登出时间, 电脑配置等。")
@RestController
@RequestMapping("/basic")
internal class BasicController : ControllerThatSyncs() {

    @Hidden                     // 🦜 : Yeah, hidden in swagger
    @GetMapping("/bye")
    fun bye(): W<String> {

        // schedule to shut down in 1s (in a new thread)
        val run = Thread(
            Runnable {
                log.info("🦜 : shutting down")
                Thread.sleep(1000)
                System.exit(0)
            }
        )
        run.start()

        /* 🦜 : Nope, scheduler is not the right fit here...Neither is the
           kotlin coroutine. */

        return W(0, "shutdown scheduled in 1s")
    }
    // 🦜 : curl localhost:8081/basic/bye


    @Autowired
    lateinit var processRegisterFailureRepo: ProcessRegisterFailureRepo

    @Autowired
    lateinit var processRegisterRepo: ProcessRegisterRepo

    @Autowired
    lateinit var bashHistoryEntryRepo: BashHistoryEntryRepo

    @Autowired
    lateinit var loginInfoRepo: LoginInfoRepo

    @Autowired
    lateinit var applicantRepo: ApplicantRepo

    @Autowired
    lateinit var fileRepo: SafeboxFileRepo

    @Autowired
    lateinit var counterService: CounterService

    @Autowired lateinit var userService: UserService

    @Operation(summary = "获取用户列表")
    @GetMapping("/getUsers")
    fun getUsers(): W<Dt.Page<Dt.UserInfo2>>  = userService.getUsers()

    @Operation(summary = "获取一堆计数器")
    @GetMapping("/getManyCounters")
    fun getManyCounters(): W<Dt.ManyCounters> {
            val ct = fillCounters()
            try {
                /* 🦜 : fill in the Daily records

                   1. x1 is the process check counter
                   2. y1 = BashHistory record in certain day (legal), y2 = illegal commands <- 0 (for now)
                   3. z1 = legal login = count of loginInfo
                   z2 = illegal login = 0 (for now)
                   z3 = root applicant = count of applicantRepo
                 */
                for (d in 0..6) {
                    log.info("🦜 : {}filling daily record for {} days ago{}", S.BLUE, d, S.NOR)
                    val di = DayInterval.nDaysFromNow(d)
                    val o = Dt.DailyRecord()
                    o.date = di.since

                    o.x1 = counterService.get(PsController.CounterKey, di.since, di.until)
                    o.y1 = bashHistoryEntryRepo.countByTimeBetween(di.sinceString, di.untilString)
                    o.y2 = 0L

                    o.z1 = loginInfoRepo.countByLoginTimeBetween(di.since, di.until)
                    o.z2 = 0L

                    o.z3 = applicantRepo.countByCreateTimeBetween(di.since, di.until)

                    // 🦜 : Add this record
                    ct.dailyRecords.add(o)
                }

                return W(0, ct)
            } catch (e: Exception) {
                log.error("📕️ error: {}", e)
                e.printStackTrace()
                throw e
            }

            // ==================================================
            // 🦜 : Get the Daily record.. (the mocked one)

            // return new Dt.W<Dt.ManyCounters>(0, Dt.ManyCounters.next());
    }

    fun fillCounters(): Dt.ManyCounters {
        log.info("🦜 : filling counters")
        val ct = Dt.ManyCounters()

        // x1 = x3 = process check counter
        // get today's 00:00 -> now
        ct.x1 = counterService.getInNDaysFromNow(PsController.CounterKey, 0)
        ct.x3 = ct.x1

        // x2 = count of must-app failure
        var spec = Specification.where<Dt.ProcessRegisterFailure?>(null)
        spec = spec.and(Dt.ProcessRegisterBase_.typeEquals_f("must"))
        ct.x2 = processRegisterFailureRepo.count(spec)

        // --------------------------------------------------
        // y1 = count of registered app
        ct.y1 = processRegisterRepo.count()

        // y2 = count of ok-app failure
        spec = Specification.where(null)
        spec = spec.and(Dt.ProcessRegisterBase_.typeEquals_f("ok"))
        ct.y2 = processRegisterFailureRepo.count(spec)

        // --------------------------------------------------
        /*
          y3 nApply of network controller. 🦜 : We use NetworkControllers.nSets,
          which got incremented when setting inbound rules.
         */
        ct.y3 = NetworkController.nSets.get()
        ct.y4 = 0L // 🦜 : how many times we successfully blocked a
        // network access. Can't get it yet...
        // y5 is the network monitored byte count..
        ct.y5 = this.getY5NetworkMonitoredByteCount()

        // --------------------------------------------------
        /*
          z1 = count of recorded bashCommand,
          z2 = illegal bashCommands = 0 (currently)
         */
        ct.z1 = bashHistoryEntryRepo.count()
        ct.z2 = 0L

        // --------------------------------------------------
        /*
          a1 : count of recorded login
          a2 : illegal login = 0 (currently)
          a3 : root user applicant = count of applicantRepo
         */
        ct.a1 = loginInfoRepo.count()
        ct.a2 = 0L
        ct.a3 = applicantRepo.count()

        // --------------------------------------------------
        /*
          b1 : count of safebox file
         */
        ct.b1 = fileRepo.count()
        return ct
    }

    @Autowired
    lateinit var networkController: NetworkController

    fun getY5NetworkMonitoredByteCount(): Long {
        // 1. call cctv4
        val m = networkController.previousInterfaceStats
        // 2. sum up the i.ioCounterStat.bytesRecv
        var sum: Long = 0L
        for (i in m.values) {
            sum = sum + i.ioCounterStat.bytesRecv
        }
        return sum
    }

    fun readOutput(inputStream: InputStream): List<String> {
        BufferedReader(InputStreamReader(inputStream)).use { output ->
            return output.lines().collect(Collectors.toList())
        }
    }

    fun prepareTeX() {
        // 0.1 the connection stats. (call get from  networkController)
        val w = networkController.getNetstats(1, 100)
        val l: List<Dt.ConnectionStat> = w.data.voList
        Utils.substituteSpecialLines(l, Dt.ConnectionStat()) //[on/off]
        //                                    ^^^ dummy object for getter of template


        // 0.2 the inbound rules
        val rs = networkController.getInboundRules()
        val l2 = rs.data
        Utils.substituteSpecialLines(l2, Dt.InboundRule2()) //[on/off]

        psController.syncIfTooOld()
        // 0.3 get the top 5 processes on sorted by cpuPercent (from processInfo2Repo)
        // var pageable: Pageable? = PageRequest.of(0, 8, Sort.by("cpuPercent").descending())
        // var page = processInfo2Repo.findAll(pageable)
        // var l3 = page.content

        /* 🦜 [2024-02-29]: We used to rely on the powerful spring-data jpa, but now we use
           the equally powerful kotlin collection */

        var l3 = psController.processList.sortedByDescending({it.cpuPercent}).take(8)

        // convert
        val l3b = Dt.ProcessInfo2.CpuTeXBar.make(l3)
        Utils.substituteSpecialLines(l3b, Dt.ProcessInfo2.CpuTeXBar()) //[on/off]

        // 0.3b get the top 5 processes on sorted by memPercent (from processInfo2Repo)
        // convert
        l3 = psController.processList.sortedByDescending({it.memPercent}).take(8)
        val l3c = Dt.ProcessInfo2.MemoryTeXBar.make(l3)
        Utils.substituteSpecialLines(l3c, Dt.ProcessInfo2.MemoryTeXBar()) //[on/off]

        // 0.3c by readBytes
        l3 = psController.processList.sortedByDescending({it.readBytes}).take(8)
        // convert
        val l3d = Dt.ProcessInfo2.ReadTeXBar.make(l3)
        Utils.substituteSpecialLines(l3d, Dt.ProcessInfo2.ReadTeXBar()) //[on/off]

        // 0.3d by writeBytes
        l3 = psController.processList.sortedByDescending({it.writeBytes}).take(8)
        // convert
        val l3e = Dt.ProcessInfo2.WriteTeXBar.make(l3)
        Utils.substituteSpecialLines(l3e, Dt.ProcessInfo2.WriteTeXBar()) //[on/off]


        // 0.4 output the process register record and process failure
        val w4 = psController.getRegisters(1, 100, null, null, null, null)
        w4.data?.let{
            val l4 = it.voList
            Utils.substituteSpecialLines(l4, Dt.ProcessRegister()) //[on/off]
        } ?: log.error("📕️ error getting registers for TeX: {}", w4.msg)

        val w5 = psController.getFailures(1, 100, "must", null, null, null)
        w5.data?.let{
            val l5 = it.voList
            Utils.substituteSpecialLines(l5, Dt.ProcessRegisterFailure()) //[on/off]
        } ?: log.error("📕️ error getting failures for TeX: {}", w4.msg)

        // 0.5 output the cpu/memory/disk stats (call `askResourceInfoFromAgent()` the cctv)
        val rpl = this.askResourceInfoFromAgent()
        val l6: MutableList<Dt.TeXKvEntry> = ArrayList()
        l6.add(Dt.TeXKvEntry("hostname", rpl.hostname))

        l6.add(Dt.TeXKvEntry("numberOfCores", String.format("%d", rpl.cpu.size)))

        l6.add(Dt.TeXKvEntry("vendorId", rpl.cpu.first().vendorId))
        l6.add(Dt.TeXKvEntry("modelName", rpl.cpu.first().modelName))
        l6.add(Dt.TeXKvEntry("model", rpl.cpu.first().model))
        l6.add(Dt.TeXKvEntry("cacheSize", String.format("%d", rpl.cpu.first().cacheSize)))
        l6.add(Dt.TeXKvEntry("cpuUsage", String.format("%.1f", rpl.cpu_percent)))
        l6.add(Dt.TeXKvEntry("cpuUsageDeg", String.format("%.0f",
                                                          rpl.cpu_percent * 3.6 // * 360 / 100
                                            )))

        // memory stats
        l6.add(Dt.TeXKvEntry("memorySize", Dt.TeXKvEntry.humanizeBytes(rpl.memory_info.total)))
        l6.add(Dt.TeXKvEntry("usedMemory", Dt.TeXKvEntry.humanizeBytes(rpl.memory_info.used)))
        l6.add(Dt.TeXKvEntry("memoryUsage", String.format("%.1f", rpl.memory_info.usedPercent)))
        l6.add(Dt.TeXKvEntry("memoryUsageDeg", String.format("%.0f", rpl.memory_info.usedPercent * 3.6))) // disk
        l6.add(Dt.TeXKvEntry("diskSize", Dt.TeXKvEntry.humanizeBytes(rpl.disk.total)))
        l6.add(Dt.TeXKvEntry("usedDisk", Dt.TeXKvEntry.humanizeBytes(rpl.disk.used)))
        l6.add(Dt.TeXKvEntry("diskPath", rpl.disk.path))
        l6.add(Dt.TeXKvEntry("fileSystemType", rpl.disk.fstype))
        l6.add(Dt.TeXKvEntry("diskUsage", String.format("%.1f", rpl.disk.usedPercent)))
        l6.add(Dt.TeXKvEntry("diskUsageDeg", String.format("%.0f", rpl.disk.usedPercent * 3.6)))

        // write kvs
        Utils.substituteSpecialLines(l6, Dt.TeXKvEntry("dummy", "dummy")) //[on/off]
    }

    @Autowired
    lateinit var psController: PsController

    @GetMapping("/pdf")
    @Operation(summary = "获取可信服务器报告pdf")
    fun pdf(): ResponseEntity<Resource> {
        log.info("🦜 invoking TeX")

        // 0. prepare the tex
        // --------------------------------------------------
        this.prepareTeX()

        // 1. invoke the tex
        // --------------------------------------------------
        val pb = ProcessBuilder("lualatex", "-shell-escape", "report.tex")
        try {
            val p = pb.start()
            p.waitFor()

            val output = readOutput(p.inputStream)
            val error = readOutput(p.errorStream)
            log.info("output of TeX: {}", output)
            log.info("error of TeX: {}", error) // 21
        } catch (e: Exception) {
            e.printStackTrace()
        }

        // 2. get the pdf
        val bytes = Files.readAllBytes(Paths.get("report.pdf"))

        return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"report.pdf\"")
            .header(HttpHeaders.CONTENT_TYPE, "application/pdf")
            .body(ByteArrayResource(bytes))
    }


    @PostMapping("/addApplicant")
    @Operation(summary = "添加申请人")
    fun addApplicant(@RequestBody o: Dt.ApplicantIn): Dt.Error {
        // make sub class

        val r = Dt.Applicant()
        r.name = o.name
        r.phone = o.phone
        r.sfz = o.sfz
        r.code = o.code
        r.department = o.department
        r.expiryTime = o.expiryTime
        r.createTime = Date()

        applicantRepo.save(r)

        return Common.ok()
    }

    @GetMapping("/rmApplicant")
    @Operation(summary = "删除申请人")
    fun rmApplicant(
        @RequestParam @Parameter(
            description = "申请人的唯一标识符",
            examples = [ExampleObject(name = "1", value = "1", description = "申请人的唯一标识符")]
        ) id: Long
    ): Dt.Error {
        applicantRepo.deleteById(id)
        return Common.ok()
    }

    @GetMapping("/getApplicants")
    @Operation(summary = "获取所有申请人")
    fun getApplicants(
        pageNum: Int, pageSize: Int,
        @RequestParam(required = false) id: Long?,
        @RequestParam(required = false) name: String?,
        @RequestParam(required = false) phone: String?,
        @RequestParam(required = false) createTime: String?,
        @RequestParam(required = false) expired: Boolean?
    ): W<Dt.Page<Dt.Applicant>?> {
        if (id != null) {
            val o = applicantRepo.findById(id)
            return if (o.isPresent) {
                W(0, Dt.Page(listOf(o.get()), 1L))
            } else {
                W(0, Dt.Page(ArrayList(), 0L))
            }
        }

        // find page
        if (pageNum < 1 || pageSize < 0) {
            return W(0, null, "不合法的页码或页大小")
        }

        // 1. prepare pageable
        val sortBys: Set<String> = setOf<String>("id", "name", "phone", "sfz", "code",
                                                 "department", "expiryTime", "createTime")

        val pageable = checkThingsAndPreparePageable(pageNum, pageSize, sortBys, "id", true, "id")

        // 2. prepare spec
        var spec = Specification.where<Dt.Applicant?>(null)
        if (name != null && !name.isEmpty()) {
            spec = spec.and(Dt.ApplicantIn_.nameContains(name))
        }
        if (!phone.isNullOrEmpty()) {
            spec = spec.and(Dt.ApplicantIn_.phoneContains(phone))
        }
        if (!createTime.isNullOrEmpty()) {
            // parse from YYYY-MM-DD HH:MM:SS
            val d = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(createTime)
            spec = spec.and(Dt.Applicant_.createTimeAfter(d))
        }

        if (expired != null) {
            spec = if (expired) {
                spec.and(Dt.ApplicantIn_.expiryTimeBefore(Date()))
            } else {
                spec.and(Dt.ApplicantIn_.expiryTimeAfter(Date()))
            }
        }

        log.info("🐸 : finding page with spec = {}", spec)
        // 3. find and return
        try {
            val page = applicantRepo.findAll(spec, pageable)
            log.info("🐸 : found page = {}", page)
            return W(0, Dt.Page(page.content.filterNotNull(), page.totalElements))
        } catch (e: Exception) {
            log.error("📕️ error: {}", e)
            throw e
        }
    }


    fun askLoginInfoFromAgent(sinceTime0: LocalDateTime, untilTime0: LocalDateTime, user0: String?): List<Dt.LoginInfo2> {

        // 1. prepare the request
        // val bl = GetLoginHistoryReq.newBuilder()
        //     .setSinceTime(sinceTime.format(fmt))
        //     .setUntilTime(untilTime.format(fmt))
        val b = pb_getLoginHistoryReq {
            sinceTime = sinceTime0.format(fmt)
            untilTime = untilTime0.format(fmt)
            user = user0 ?: ""
        }

        // 2. call the agent
        val r: pb_GetLoginHistoryRpl
        try {
            runBlocking {
                r = c.stub.getLoginHistory(b)
            }
        } catch (e: StatusException) {
            val msg = e.status.description
            log.error(S.RED + "📕️ RPC failed: {}" + S.NOR, e.status)
            throw RuntimeException(msg)
        }

        // 3. pb -> java
        val l: MutableList<Dt.LoginInfo2> = ArrayList()
        for (i in r.loginInfosList) {
            l.add(Dt.LoginInfo2.convertFromPb(i))
        }

        // 4. return
        return l
    }


    // 🦜 : First let's override the `sync()` method
    override fun sync() {
        // 1. call the agent to get the list
        // --------------------------------------------------
        val untilTime = LocalDateTime.now()
        // a year ago
        val sinceTime = untilTime.minusYears(1)
        val l = this.askLoginInfoFromAgent(sinceTime, untilTime, "")

        // ^^^ get all the login history in the past year

        // 2. update the repo
        // --------------------------------------------------
        loginInfoRepo.deleteAll()
        loginInfoRepo.saveAll(l)

    }

    @GetMapping("/cctv1")
    @Operation(
        summary = "获取系统资源信息",
        description = "🦜: 获取系统资源信息，包括CPU,内存,磁盘,IO等。一般用websocket去收听`/ws/cctv1`，这个会每个几秒传一个这个。"
    )
    fun cctv1(): W<Dt.GetResourceInfoRpl2>  = W(0, this.askResourceInfoFromAgent())

    @Autowired
    lateinit var t: ThreadPoolTaskScheduler

    @Autowired
    lateinit var json: ObjectMapper

    @GetMapping("/getLoginHistory")
    @Operation(summary = "获取用户登录历史")
    fun getLoginHistory(
        @RequestParam(defaultValue = "1") @Parameter(
            description = "页码（从1开始）",
            examples = [ExampleObject(name = "1", value = "1", description = "第一页")]
        ) pageNum: Int,
        @RequestParam(defaultValue = "2") @Parameter(
            description = "每页多少条",
            examples = [ExampleObject(name = "2", value = "2", description = "每页2条")]
        ) pageSize: Int,
        @RequestParam(required = false) @Parameter(
            description = "用户名",
            examples = [ExampleObject(name = "aaa", value = "aaa", description = "查找用户aaa的记录"), ExampleObject(
                name = "",
                value = "",
                description = "查找所有用户的记录"
            )]
        ) user: String?,
        @RequestParam(required = false) @Parameter(
            description = "某个时间以后登录的",
            required = false,
            examples = [ExampleObject(
                name = "2021-01-01 00:00:00",
                value = "2021-01-01 00:00:00",
                description = "某个时间后登录的"
            )]
        ) sinceTime: String?,
        @RequestParam(required = false) @Parameter(
            description = "某个时间以前登录的",
            required = false,
            examples = [ExampleObject(
                name = "2021-01-01 00:00:00",
                value = "2021-01-01 00:00:00",
                description = "某个时间前登录的"
            )]
        ) untilTime: String?,
        @RequestParam(required = false) @Parameter(
            description = "按哪行排序",
            required = false,
            examples = [ExampleObject(
                name = "loginTime",
                value = "loginTime",
                description = "默认按登录时间排序"
            ), ExampleObject(name = "logoutTime", value = "logoutTime", description = "按登出时间排序")]
        ) sortBy: String?,
        @RequestParam(required = false) @Parameter(description = "是否升序", required = false) asc: Boolean?
    ): W<Dt.Page<Dt.LoginInfo2?>> {
        log.info(
            "{}🐸 : getLoginHistory called with pageNum={}, pageSize={}, user={}, sinceTime={}, untilTime={}, sortBy={}, asc={} {}",
            S.CYAN, pageNum, pageSize, user, sinceTime, untilTime, sortBy, asc, S.NOR
        )

        this.syncIfTooOld()

        // 1. prepare pageable
        val sortBys: Set<String> = setOf("loginTime", "logoutTime")
        val pageable = checkThingsAndPreparePageable(pageNum, pageSize, sortBys, sortBy, asc, "loginTime")
        val page: Page<Dt.LoginInfo2?>

        // 2. prepare spec
        var spec = Specification.where<Dt.LoginInfo2?>(null)
        if (user != null && user.isNotEmpty()) {
            spec = spec.and(Dt.LoginInfo2_.userContains(user))
        }
        if (sinceTime != null && sinceTime.isNotEmpty()) {
            val sinceTimeLocalDateTime = LocalDateTime.parse(sinceTime, fmt)
            val sinceTimeDate = new2Old(sinceTimeLocalDateTime)
            spec = spec.and(Dt.LoginInfo2_.loginTimeAfter(sinceTimeDate))
        }

        if (!untilTime.isNullOrEmpty()) {
            val untilTimeLocalDateTime = LocalDateTime.parse(untilTime, fmt)
            val untilTimeDate = new2Old(untilTimeLocalDateTime)
            spec = spec.and(Dt.LoginInfo2_.loginTimeBefore(untilTimeDate))
        }

        // 3. find and return
        page = loginInfoRepo.findAll(spec, pageable)
        // log.info("{}🐸 : found page of {}", S.CYAN, page.getTotalElements() , S.NOR);
        return W(0, Dt.Page(page.content, page.totalElements))
    }

    @EventListener // 🦜 : What's this ? 🐢 : This is the spring magic to run something on startup.
    fun init(e: ApplicationReadyEvent?) {
        // System.out.println("⚙️ Initializing  BasicController");

        // 0.1 mock some Applicants
        // --------------------------------------------------

        applicantRepo.saveAll(Dt.Applicant.mockList(11))


        // 1. schedule the task
        // --------------------------------------------------
        t.scheduleAtFixedRate(
            {
                try {
                    val r = this.askResourceInfoFromAgent()
                    Wc1.say(json.writeValueAsString(r))
                } catch (ex: Exception) {
                    log.error("🐸 : error saying cctv4", ex)
                }
            },
            Duration.of(5, ChronoUnit.SECONDS)
        ) // every two seconds, boardcast to all listener of /hi

        // 2. try prepare the TeX
        // --------------------------------------------------
        try {
            prepareTeX()
        } catch (ex: Exception) {
            log.error("🐸 : error preparing TeX", ex)
            ex.printStackTrace()
        }
        log.info(S.BLUE + "🐸 📗️: TeX prepared" + S.NOR)
    }

    // --------------------------------------------------
    fun askResourceInfoFromAgent(): Dt.GetResourceInfoRpl2 {
        val b = pb_empty({})
        val r: pb_GetResourceInfoRpl

        // rpc GetResourceInfo (Empty) returns (GetResourceInfoRpl) {}
        try {
            runBlocking{
                r = c.stub.getResourceInfo(b)
            }
        } catch (e: StatusException) {
            val msg = e.status.description
            log.error(S.RED + "📕️ RPC failed: {}" + S.NOR, e.status)
            throw RuntimeException(msg)
        }

        // --------------------------------------------------
        // 2. pb -> Java
        return Dt.GetResourceInfoRpl2.convertFromPb(r)
    }
} // class BasicController


// class NetworkController
@Tag(name = "网络", description = "🦜: 这个可以看网络信息，包括哪些口是被开的，还有连接啥的。")
@RestController
@RequestMapping("/net")
internal class NetworkController : ControllerThatSyncs() {
    var previousInterfaceStats: Map<Int, Dt.InterfaceStat> = HashMap()

    @Autowired
    lateinit var t: ThreadPoolTaskScheduler

    @Autowired
    lateinit var json: ObjectMapper

    // --------------------------------------------------
    // --------------------------------------------------
    // 🦜 : netstats functionality
    fun convertFromPb(i: pb_ConnectionStat): Dt.ConnectionStat {
        val i2 = Dt.ConnectionStat()
        i2.fd = i.fd
        i2.family = i.family
        i2.type = i.type

        i2.localAddr = Dt.AddrPort(
            addr = i.localAddr.addr,
            port = i.localAddr.port
        )

        i2.remoteAddr = Dt.AddrPort(
            addr = i.remoteAddr.addr,
            port = i.remoteAddr.port
        )

        i2.status = i.status
        i2.uids = i.uidsList.toList() //copy the list
        i2.pid = i.pid
        i2.exeName = i.exeName
        i2.closed = true

        return i2
    }


    // --------------------------------------------------
    // cctv4 functionality
    @EventListener // 🦜 : What's this ? 🐢 : This is the spring magic to run something on startup.
    fun init(e: ApplicationReadyEvent?) {
        println("⚙️ Initializing NetworkController tasks")

        t.scheduleAtFixedRate(
            {
                try {
                    val m = askInterfaceInfoFromAgent()
                    val l: List<Dt.InterfaceStat> = ArrayList(m.values)
                    Wc4.say(json.writeValueAsString(l))
                } catch (ex: Exception) {
                    log.error("🐸 : error saying cctv4", ex)
                }
            },
            Duration.of(5, ChronoUnit.SECONDS)
        ) // every two seconds, boardcast to all listener of /hi
    }

    @GetMapping("/cctv4")
    @Operation(
        summary = "获取网络接口信息",
        description = "🦜: 获取网络接口信息，包括每个口的ip,mac,流量等。一般用websocket去收听`/ws/cctv4`，这个会每个几秒传一个这个。"
    )
    fun cctv4(): W<List<Dt.InterfaceStat>> {
        val m = this.askInterfaceInfoFromAgent()
        return W(0, ArrayList(m.values))
    }


    @Synchronized
    fun askInterfaceInfoFromAgent(): Map<Int, Dt.InterfaceStat> {
        // val b = Empty.newBuilder().build()
        val b = pb_empty({})
        val r: pb_GetNICInfoRpl

        // 1. call the agent
        // rpc GetNICInfo (Empty) returns (GetNICInfoRpl) {}
        runBlocking {
            try {
                r = c.stub.getNICInfo(b)
            } catch (e: StatusException) {
                val msg = e.status.description
                log.error(S.RED + "📕️ RPC failed: {}" + S.NOR, e.status)
                throw RuntimeException(msg)
            }
        }

        // 2. pb -> Java
        val m: MutableMap<Int, Dt.InterfaceStat> = HashMap()
        for (i in r.interfaceStatsList) {
            val i2 = convertFromPb(i)

            // --------------------------------------------------
            // 🦜 :If we have lastTimeWeGotNICiousInterfaceStats, we can calculate the speed
            if (previousInterfaceStats.containsKey(i2.index)) {
                val i3 = previousInterfaceStats[i2.index]!! //throw if not new
                // 🦜 : calculate the speed using `LocalDateTime lastSyncTime`
                val secSinceLastSync = Duration.between(lastTimeWeGotNIC, LocalDateTime.now()).seconds
                // 🦜 : calculate the speed using `IOCounterStat ioCounterStat`
                i2.ioCounterStat.recvSpeedBytePerSec =
                    (i2.ioCounterStat.bytesRecv - i3.ioCounterStat.bytesRecv) / secSinceLastSync
                i2.ioCounterStat.sentSpeedBytePerSec =
                    (i2.ioCounterStat.bytesSent - i3.ioCounterStat.bytesSent) / secSinceLastSync
            } else {
                // set the speed to 0
                i2.ioCounterStat.recvSpeedBytePerSec = 0L
                i2.ioCounterStat.sentSpeedBytePerSec = 0L
            }

            m[i2.index] = i2
        }

        // 🦜 : save the current interface stats and update time
        previousInterfaceStats = m
        lastTimeWeGotNIC = LocalDateTime.now()

        return m
    }

    var lastConnectionStats : MutableList<Dt.ConnectionStat> = ArrayList()
    override fun sync() {
        // <2024-02-28: netstats functionality is rejected, so no need to persist anything from agent>
        var b = pb_empty({})
        var r : pb_GetNetstatsInfoRpl
        try {
            runBlocking {
                r = c.stub.getNetstatsInfo(b)
            }
        } catch (e: StatusException) {
            val msg = e.status.description
            log.error(S.RED + "📕️ RPC failed: {}" + S.NOR, e.status)
            throw RuntimeException(msg)
        }

        // 🦜 : pb -> java and save
        lastConnectionStats.clear()
        for (i in r.connectionStatsList) {
            val i2 = convertFromPb(i)
            lastConnectionStats.add(i2)
        }
    }

    @GetMapping("/get")
    @Operation(summary = "获取网络连接信息")
    fun getNetstats(pageNum: Int, pageSize: Int): W<Dt.Page<Dt.ConnectionStat>> {
            // 🦜 : Update in [2024-02-29], we don't need to persist anything from agent
            this.syncIfTooOld()
            // return getPage(repo, pageNum, pageSize)

            // 🦜 : Use the powerful kotlin collection
            if (pageSize <= 0 || pageNum <= 0) {
                throw IllegalArgumentException("pageNum or pageSize is illegal")
            }
            val l = this.lastConnectionStats.chunked(pageSize)
            // 🦜 : kotlin collection is smart enough to deal with pageSize that's too big (i.e. pageSize > l.size)

            if (pageNum > l.size) {
                return W(0, Dt.Page(emptyList(), 0))
            }
            return W(0, Dt.Page(l[pageNum - 1], l.size.toLong()))
        }

    // --------------------------------------------------
    // firewall functionality
    /*

      public static class InboundRule2{
      String type;            // tcp | udp | icmp | all
      String portRange;
      String source;
      Boolean ipv6;
    */
    var inboundRules: List<Dt.InboundRule2> = ArrayList()

    @GetMapping("/getInboundRules")
    @Operation(summary = "获取入站规则")
    fun getInboundRules(): W<List<Dt.InboundRule2>> {
        return W(0, inboundRules)
    }

    @PostMapping("/setInboundRules")
    @Operation(
        summary = "设置入站规则",
        description = "🦜: 设置入站规则. `type`可以是`tcp`, `udp`, `icmp`, `all`。`portRange`是一个范围，比如`80:90` 也可以是单个如`80`。" +
                "`source`是一个或多个ip地址如`1.2.3.4`,`1.2.3.4/24`, `0.0.0.0/0`,`fd00::/8`。"
    )
    fun setInboundRules(@RequestBody inboundRules: List<Dt.InboundRule2>): Dt.Error {
        nSets.incrementAndGet() // 🦜 : increments the counter

        this.inboundRules = inboundRules

        /*
          🦜 call the agent: rpc SetInboundRules (SetInboundRulesReq) returns (HelloReply) {} // 🦜 : return "ok" 
          message SetInboundRulesReq {
          repeated InboundRule rules = 1;
          }
          1. prepare the req
        */
        val b = pb_setInboundRulesReq({ // Dsl here
                                          for (i in inboundRules) {
                                              this.rules.add(i.convertToPb())
                                          }
                                      })
        // log.info("🐸 : setInboundRules, inboundRules: {}", inboundRules)
        // log.info("🐸 : setInboundRules in pb , b: {}", b)

        // 2. call the agent
            try {
                runBlocking{
                    val r = c.stub.setInboundRules(b)
                    log.info("🐸 : setInboundRules, agent answered: {}", r.msg)
                }
            } catch (e: StatusException) {
                val msg = e.status.description
                log.error(S.RED + "📕️ RPC failed: {}" + S.NOR, e.status)
                return Common.bad(msg)
            }
            /*
             🦜 : In kotlin, the stub raises StatusException, but not StatusRuntimeException as in java
             */

        return Common.ok()
    }

    companion object {
        var lastTimeWeGotNIC: LocalDateTime = LocalDateTime.now().minusDays(1)

        private fun convertFromPb(i: pb_InterfaceStat): Dt.InterfaceStat {
            val i2 = Dt.InterfaceStat(
                index = i.index,
                mtu = i.mtu,
                name = i.name,
                hardwareAddr = i.hardwareAddr,
                addrs = i.addrsList,
                flags = i.flagsList,
                defaultGateway = i.defaultGateway
            )

            i2.ioCounterStat.let{
                it.bytesSent = i.ioCounterStat.bytesSent
                it.bytesRecv = i.ioCounterStat.bytesRecv
                it.packetsSent = i.ioCounterStat.packetsSent
                it.packetsRecv = i.ioCounterStat.packetsRecv
                it.errin = i.ioCounterStat.errin
                it.errout = i.ioCounterStat.errout
                it.dropin = i.ioCounterStat.dropin
                it.dropout = i.ioCounterStat.dropout
            }

            return i2
        }

        var nSets: AtomicLong = AtomicLong(1) // 🦜 : how many times the `/SetInboundRules` is called.
    }
} // class NetworkController


internal interface BashHistoryEntryRepo : JpaRepository<Dt.BashHistoryEntry2, Long>,
    JpaSpecificationExecutor<Dt.BashHistoryEntry2> {
    fun countByTimeBetween(since: String?, until: String?): Long
}

@Tag(name = "命令历史记录", description = "🦜: 用这个服务来查看命令历史记录吧。")
@RestController
@RequestMapping("/cmdHistory")
internal class CmdHistoryController : ControllerThatSyncs() {

    @GetMapping("/throw")
    fun throwException(): String {
        throw RuntimeException("just a test")
    }

    // rpc GetBootTime (Empty) returns (HelloReply) {}
    @GetMapping("/get")
    @Operation(summary = "获取用户命令历史")
    fun get(
        @RequestParam(defaultValue = "1") @Parameter(
            description = "页码（从1开始）",
            examples = [ExampleObject(name = "1", value = "1", description = "第一页")]
        ) pageNum: Int,
        @RequestParam(defaultValue = "2") @Parameter(
            description = "每页多少条",
            examples = [ExampleObject(name = "2", value = "2", description = "每页2条")]
        ) pageSize: Int,
        @RequestParam(required = false) @Parameter(
            description = "ID",
            examples = [ExampleObject(name = "1", value = "1", description = "查找ID=1的记录")]
        ) id: Long?,
        @RequestParam(required = false) @Parameter(
            description = "用户名",
            examples = [ExampleObject(name = "aaa", value = "aaa", description = "查找用户aaa的记录")]
        ) user: String?,
        @RequestParam(required = false) @Parameter(
            description = "按哪行排序",
            required = false,
            examples = [ExampleObject(name = "time", value = "time", description = "默认按时间排序")]
        ) sortBy: String?,
        @RequestParam(required = false) startTime: String?,
        @RequestParam(required = false) endTime: String?,
        @RequestParam(required = false) @Parameter(description = "是否升序", required = false) asc: Boolean?
    ): W<Dt.Page<Dt.BashHistoryEntry2?>> {
        if (id != null) {
            val o = bashHistoryEntryRepo.findById(id)
            if (o.isPresent) {
                return W(0, Dt.Page(listOf(o.get()), 1L))
                // a page of 1
            } else {
                throw RuntimeException("id not found")
            }
        }

        this.syncIfTooOld()

        // find page
        // 1. prepare pageable
        // --------------------------------------------------
        val sortBys: Set<String> = setOf("id", "user", "time", "cmd")
        val pageable = checkThingsAndPreparePageable(pageNum, pageSize, sortBys, sortBy, asc, "id")

        // 2. prepare spec
        // --------------------------------------------------
        var spec = Specification.where<Dt.BashHistoryEntry2?>(null)
        if (!user.isNullOrEmpty()) {
            spec = spec.and(Dt.BashHistoryEntry2_.userContains(user))
        }

        if (startTime != null) {
            spec = spec.and(Dt.BashHistoryEntry2_.timeAfter(startTime))
        }

        if (endTime != null) {
            spec = spec.and(Dt.BashHistoryEntry2_.timeBefore(endTime))
        }

        // 3. find and return
        // --------------------------------------------------
        val page = bashHistoryEntryRepo.findAll(spec, pageable)
        return W(0, Dt.Page(page.content, page.totalElements))
    }

    @Autowired
    lateinit var bashHistoryEntryRepo: BashHistoryEntryRepo //🦜 : This is a transient db..

    // 🦜 : First let's override the `sync()` method
    override fun sync() {
        // --------------------------------------------------
        // sync the cmd history
        bashHistoryEntryRepo.deleteAll()
        val l2 = this.askBashHistoryFromAgent()
        bashHistoryEntryRepo.saveAll(l2)
    }

    fun askBashHistoryFromAgent(): List<Dt.BashHistoryEntry2> {
        // 1. prepare the request
        val b = pb_empty({})
        // 2. call the agent
        val r: pb_GetBashHistoryRpl

        runBlocking {
            try {
                r = c.stub.getBashHistory(b)
            } catch (e: StatusException) {
                val msg = e.status.description
                log.error(S.RED + "📕️ RPC failed: {}" + S.NOR, e.status)
                throw RuntimeException(msg)
            }
        }

        // 3. pb -> java
        val l: MutableList<Dt.BashHistoryEntry2> = ArrayList()
        for (i in r.entriesList) {
            l.add(Dt.BashHistoryEntry2.convertFromPb(i))
        }

        // 4. return
        return l
    }

    @EventListener
    fun init(e: ApplicationReadyEvent?) {
        // 0. mock some CmdHistory [reseved for now, the /old/bashHistory/... is used instead]
        // --------------------------------------------------
        // cmdHistoryRepo.saveAll(Dt.CmdHistory.mockList(20));
        // 🦜 : Here we can ask the agent now, so no need to mock anything.
    }
} // class CmdHistoryController


// --------------------------------------------------
interface SafeboxRecordRepo : JpaRepository<Dt.SafeboxRecord, String>

/**
 * @brief Where all files in Private folder are stored.
 *
 * 🦜 : this in fact is just a temp repo, every time `sync()` is called, the
 * repo will be cleared and re-populated with data from the agent.
 */
interface SafeboxFileRepo : JpaRepository<Dt.SafeboxFile, Long> {
    fun findByUser(user: String?, pageable: Pageable): Page<Dt.SafeboxFile>

    // find last access time by user
    fun findTopByUserOrderByLastAccessTimeDesc(user: String?): Dt.SafeboxFile?

    // count the number of files by user
    fun countByUser(user: String?): Long

    // find all files that contains fileName
    fun findByUserAndFileNameContaining(user: String?, fileName: String, pageable: Pageable): Page<Dt.SafeboxFile>
}


@Tag(
    name = "文件保险箱", description = "🦜: *每个用户*都可以创建*一个*，其实就是个文件夹，放在这里的文件大概会安全吧。" +
            "🐢 那可不可以每个用户创建多个保险箱可不可以？ 🦜 :....感觉应该行，但得研究一下，现在这个功能是调用了一个现成的工具" +
            "`ecryptfs-setup-private`(由Ubuntu开发)。要想多个保险箱的话就得自己写工具了...."
)
@RestController
@RequestMapping("/safebox")
internal class SafeboxController : ControllerThatSyncs() {
    @Autowired
    lateinit var fileRepo: SafeboxFileRepo

    @Autowired
    lateinit var repo: SafeboxRecordRepo


    /**
     * @brief Create a new safebox
     */
    @Operation(
        summary = "新建保险箱", description = "这个会用传进来的用户名和密码登录并创建一个保险箱，" +
                "之前创建过的话不行。如果一切顺利话会在那个用户的`\$HOME`下创建一个`Private/`文件夹。" +
                "(所以路径一般就是`\$HOME/Private/`)。 这个文件夹会在用户登录时被挂载(`mount`)， 而在用户退出时被卸载(`umount`)。"
    )
    @PostMapping("/create")
    fun createSafebox(@RequestBody o: Dt.SafeboxRecord): Dt.Error {
        if (repo.existsById(o.user)) {
            // return Common.bad("user already has safebox", HttpStatus.CONFLICT);
            return Common.bad("user already has safebox")
        }

        // json to pb
        // val b = PswdLogin.newBuilder().setUsr(o.user).setPswd(o.passwd).build()
        val b = pb_pswdLogin({ usr = o.user; pswd = o.passwd })
        val r: pb_HelloReply
        try {
            log.info(S.GREEN + "📕️ Creating safebox for : {}" + S.NOR, o.user)
            runBlocking {
                r = c.stub.createSafebox(b)
            }
        } catch (e: StatusException) {
            val msg = e.status.description
            log.error(S.RED + "📕️ RPC failed: {}" + S.NOR, e.status)
            return Common.bad(msg)
        }
        // safe the SafeboxRecord to db
        repo.save(o)
        return Common.ok()
    }

    @Operation(summary = "获取保险箱信息", description = "这里获取的就是之前`/create`时传进来的信息。")
    @GetMapping("/get")
    fun getPage(pageNum: Int, pageSize: Int): W<Dt.Page<Dt.SafeboxWithMore>?> {
        // return safeboxService.getSafeboxBasicInfosByPages(pageNum,pageSize);
        // if(pageNum < 1 || pageSize < 0){
        //     throw new IllegalArgumentException("pageNum or pageSize is illegal");
        // }

        if (pageNum < 1 || pageSize < 0) {
            return W(0, null, "不合法的页码或页大小")
        }

        // //从数据库中查询
        // // --------------------------------------------------
        val page = repo.findAll(PageRequest.of(pageNum - 1, pageSize))

        // static public class SafeboxWithMore extends SafeboxRecord {
        //     public Date lastAccessTime;
        //     public Long numFiles;
        // }
        // Extend the SafeboxRecord with the number of files in the safebox
        val l: MutableList<Dt.SafeboxWithMore> = ArrayList()
        for (i in page.content) {
            val o = Dt.SafeboxWithMore()
            o.user = i.user
            o.passwd = i.passwd
            val f = fileRepo.findTopByUserOrderByLastAccessTimeDesc(i.user)

            // if (f != null) {
            //     o.lastAccessTime = f.lastAccessTime
            // } else {
            //     o.lastAccessTime = Date()
            // }
            o.lastAccessTime = f?.lastAccessTime ?: Date()

            o.numFiles = fileRepo.countByUser(i.user)
            l.add(o)
        }

        // return ResponseEntity.status(HttpStatus.OK)
        //     .body(new Dt.Page<Dt.SafeboxRecord>(page.getContent(),page.getTotalElements()));
        return W(0, Dt.Page(l, page.totalElements))
    }

    @GetMapping("/ls")
    @Operation(summary = "列出某用户保险箱中的文件")
    fun ls(
        @Parameter(
            description = "用户名",
            examples = [ExampleObject(name = "aaa", value = "aaa", description = "用户aaa")]
        ) user: String?,
        @Parameter(
            description = "页码（从1开始）",
            examples = [ExampleObject(name = "1", value = "1", description = "第一页")]
        ) pageNum: Int,
        @Parameter(
            description = "每页多少条",
            examples = [ExampleObject(name = "2", value = "2", description = "每页2条")]
        ) pageSize: Int,
        @Parameter(
            description = "按哪行排序",
            required = false,
            examples = [ExampleObject(
                name = "fileName",
                value = "fileName",
                description = "默认按文件名排序"
            ), ExampleObject(
                name = "size",
                value = "size",
                description = "按文件大小排序"
            ), ExampleObject(
                name = "lastModifyTime",
                value = "lastModifyTime",
                description = "按最后修改时间排序"
            ), ExampleObject(
                name = "lastAccessTime",
                value = "lastAccessTime",
                description = "按最后访问时间排序"
            ), ExampleObject(name = "permissions", value = "permissions", description = "按权限排序")]
        ) sortBy: String?,
        @Parameter(description = "是否升序", required = false) asc: Boolean?,
        @Parameter(description = "文件名 (若无，找全部)", required = false) fileName: String?
    ): W<Dt.Page<Dt.SafeboxFile>> {
        // if the cache is too old, sync, 🐢 : kinda lazy eval, ohhh, greener than a scheduled task

        this.syncIfTooOld() // call the base class's method

        require(!(pageNum < 1 || pageSize < 0)) { "pageNum or pageSize is illegal" }

        val sortBys: Set<String?> =
            HashSet(mutableListOf<String?>("fileName", "size", "lastModifyTime", "lastAccessTime", "permissions"))
        val pageable = checkThingsAndPreparePageable(pageNum, pageSize, sortBys, sortBy, asc, "fileName")

        if (!fileName.isNullOrEmpty()) {
            val page = fileRepo.findByUserAndFileNameContaining(user, fileName, pageable)
            return W(0, Dt.Page(page.content, page.totalElements))
        }

        /*
          if (sortBy == null || sortBy.isEmpty()) {
          sortBy = "fileName";
          } else if (!sortBys.contains(sortBy)){
          throw new IllegalArgumentException("sortBy is illegal, it can only be one of " + sortBys.toString());
          }
          // --------------------------------------------------
          // 🦜 : use the powerfull spring-data, so powerful
          Sort sort = Sort.by(sortBy);
          if (asc != null && !asc) {
          sort = sort.descending();
          }else{
          sort = sort.ascending();
          }
          Pageable pageable = PageRequest.of(pageNum-1, pageSize, sort);
        */
        // refactored ^v
        val page = fileRepo.findByUser(user, pageable)
        return W(0, Dt.Page(page.content, page.totalElements))
    }

    /**
     * @brief Update the sql.
     *
     * 🦜 : This will clear the `fileRepo` and then for all user who has safebox
     * (stored in `repo`), it will get the files in the safebox and save them in `fileRepo`.
     */
    override fun sync() {
        fileRepo.deleteAll()
        repo.findAll().forEach({ r: Dt.SafeboxRecord ->
            try {
                log.info(S.GREEN + "⚙️ : syncing safebox contents for user : {}" + S.NOR, r.user)
                this.syncOne(r)
            } catch (e: Throwable) {
                log.error(S.RED + "❌️ : error syncing safebox for user : {}" + S.NOR, r.user, e)
                // do nothing
            }
        })
    }

    @Synchronized
    fun syncOne(o: Dt.SafeboxRecord) {
        // Form the SafeboxFile from the DescSafeboxRpl
        // val b = PswdLogin.newBuilder().setUsr(o.user).setPswd(o.passwd).build()
        val b = pb_pswdLogin({ usr = o.user; pswd = o.passwd })
        runBlocking{
            val r = c.stub.descSafebox(b)

            for (i in r.filesList) {
                val f = Dt.SafeboxFile()

                // java from pb
                f.user = o.user // user is from `o`

                f.fileName = i.fileName
                f.size = i.size
                f.lastModifyTime = new2Old(LocalDateTime.parse(i.lastModifyTime, fmt))
                f.lastAccessTime = new2Old(LocalDateTime.parse(i.lastAccessTime, fmt))
                f.permissions = i.permissions

                fileRepo.save(f)
            }
        }
    }
}                               //class SafeboxController


internal interface ProcessRegisterFailureRepo : JpaRepository<Dt.ProcessRegisterFailure, Long>,
                                                JpaSpecificationExecutor<Dt.ProcessRegisterFailure>

internal interface ProcessRegisterRepo : JpaRepository<Dt.ProcessRegister, Long>,
                                         JpaSpecificationExecutor<Dt.ProcessRegister> {
    fun findByType(type: String): List<Dt.ProcessRegister>
    fun countByExeName(exeName: String): Long // 🦜 : For now, we use this to check whether a process is ok to run or not

    // by exeName and userName
    fun countByExeNameAndUserName(exeName: String, userName: String): Long
}

interface OneoffProcessInfo2Repo : JpaRepository<Dt.OneoffProcessInfo2, Long>,
                                   JpaSpecificationExecutor<Dt.OneoffProcessInfo2> {}


@Component
internal class UserService : ControllerThatSyncs(){
    // 🦜 : Provide the users to the controllers

    fun getUsers(): W<Dt.Page<Dt.UserInfo2>> {
        /*
         [2024-03-04]: Update, we cache this result.
         */

       syncIfTooOld()

        // return W(0, Dt.Page(users, r.usersCount.toLong()))
        return W(0, Dt.Page(cachedUsers.values.toList(), cachedUsers.size.toLong()))
    }

    val cachedUsers : ConcurrentHashMap<Int, Dt.UserInfo2> = ConcurrentHashMap()

    override fun sync(){
        //  get the user list
        /*
         🦜 : There won't be many, so we don't bother sql|paging here
         */
        val b = pb_empty({})
        val r: pb_GetUsersRpl
        try {
            runBlocking {
                r = c.stub.getUsers(b)
            }
        } catch (e: StatusException) {
            val msg = e.status.description
            log.error(S.RED + "📕️ RPC failed: {}" + S.NOR, e.status)
            throw RuntimeException(msg)
        }

        // pb -> java
        this.cachedUsers.clear()
        for (u in r.usersList) {
            // users.add()
            val u0 = Dt.UserInfo2.convertFromPb(u)
            this.cachedUsers[u0.uidInt] = u0
        }
    }
}

@Tag(name = "进程注册", description = "🐢 : 进程注册相关的接口")
@RestController
@RequestMapping("/process")
internal class PsController : ControllerThatSyncs() {
    // public static AtomicLong nChecks = new AtomicLong(1);
    // ^v 🦜 : we switched from ^ to v 
    @Autowired lateinit var counterService: CounterService

    @Autowired lateinit var processRegisterFailureRepo: ProcessRegisterFailureRepo // failure-db

    @Autowired lateinit var processRegisterRepo: ProcessRegisterRepo // records-db

    @Autowired lateinit var oneoffProcessInfo2Repo: OneoffProcessInfo2Repo

    @Autowired lateinit var userService: UserService //🦜 : For the `cachedUsers` kv

    @Autowired lateinit var t: ThreadPoolTaskScheduler

    val toBeProcessedOneoffProcessInfo: ConcurrentLinkedQueue<Dt.OneoffProcessInfo2> = ConcurrentLinkedQueue()
    @EventListener(ApplicationReadyEvent::class)
    fun init() {
        log.info("🐢 : PsController init")
        addBuiltInProcesses()

        /* <2024-03-02 Sat> 🦜: start a thread to fetch OneoffProcessInfo from
           the agent

           rpc StreamOneoffProcessInfo (Empty) returns (stream OneoffProcessInfo) {}
         */
        val run = Thread (
            Runnable {
                log.info("🐢 : starting to get OneoffProcessInfo from agent")
                runBlocking {
                    try {
                        val r = c.stub.streamOneoffProcessInfo(pb_empty({}))
                        r.collect {
                            val p = Dt.OneoffProcessInfo2.convertFromPb(it)
                            log.info("🐢 : got OneoffProcessInfo from agent: {}", p)
                            oneoffProcessInfo2Repo.save(p)

                            /*
                             🦜 : Now, we also throw a Failure record if the process is not in the register

                             🐢 : What about add these into a queue, and only check these when queried?

                             🦜 : That's a good, then we need a fifo queue right?

                             🐢 : Yeah, just a concurrent array will do. Here we use an java.util.ConcurrentLinkedQueue
                             */
                            toBeProcessedOneoffProcessInfo.add(p)
                        }
                    } catch (e: StatusException) {
                        log.error("🐢 : {}Error getting OneoffProcessInfo: {}{}", S.RED, e.message, S.NOR)
                    }
                }
            }
        )
        run.start()
        log.info("🐢 : started the OneoffProcessInfo thread")

        /* [🦜 : For this controller, we probably wanna manually `sync()` at a
           regular interval, so we can check in and see if the processes are their...
         */

        t.scheduleAtFixedRate({
                                  sync()
                                  lastSyncTime = LocalDateTime.now()
                              }, Duration.of(10, ChronoUnit.SECONDS))
    }

    fun addBuiltInProcesses() {
        /*
         🦜 : There're some processes that are ok to run on a usual GNU/Linux system, we add them here.
         */

        val l: MutableList<Dt.ProcessRegister> = ArrayList()
        // l.add(new Dt.Dt.ProcessRegister("sshd", "the ssh server"));
        l.add(Dt.ProcessRegister.MakeOkRegister("init", "the init process"))
        l.add(Dt.ProcessRegister.MakeOkRegister("systemd-oomd", "the oom killer"))
        l.add(Dt.ProcessRegister.MakeOkRegister("systemd-resolved", "the dns resolver"))
        l.add(Dt.ProcessRegister.MakeOkRegister("systemd-timesyncd", "the time syncer"))
        l.add(Dt.ProcessRegister.MakeOkRegister("systemd-logind", "the login manager"))
        l.add(Dt.ProcessRegister.MakeOkRegister("systemd-machined", "the machine manager"))
        l.add(Dt.ProcessRegister.MakeOkRegister("systemd-journald", "the journal manager"))
        l.add(Dt.ProcessRegister.MakeOkRegister("systemd-udevd", "the device manager"))
        l.add(Dt.ProcessRegister.MakeOkRegister("systemd", "the init process"))
        l.add(Dt.ProcessRegister.MakeOkRegister("cron", "the cron daemon"))
        l.add(Dt.ProcessRegister.MakeOkRegister("@dbus-daemon", "the dbus daemon"))
        l.add(Dt.ProcessRegister.MakeOkRegister("dbus-daemon", "the dbus daemon"))
        l.add(Dt.ProcessRegister.MakeOkRegister("NetworkManager", "the network manager"))
        l.add(Dt.ProcessRegister.MakeOkRegister("git", "the version control system"))
        l.add(Dt.ProcessRegister.MakeOkRegister("sh", "the shell"))
        l.add(Dt.ProcessRegister.MakeOkRegister("docker","the container manager"))
        l.add(Dt.ProcessRegister.MakeOkRegister("com.docker.cli", "the container manager"))
        l.add(Dt.ProcessRegister.MakeOkRegister("wc", "the word count command-line tool"))

        processRegisterRepo.saveAll(l)
    }

    val processList: MutableList<Dt.ProcessInfo2> = ArrayList()
    /*
     🦜 : [2024-02-29] we don't persist ProcessInfo2 in db any more, instead we use a List `processList`
     */

    @Synchronized
    override fun sync() {
        // 1. increment counter
        // nChecks.incrementAndGet();
        counterService.inc(CounterKey)

        // 1.1 get the process info from agent
        var rpl: pb_GetProcessInfoRpl
        try {
            runBlocking {
                rpl = c.stub.getProcessInfo(pb_empty({}))
                // 1.2 clean and update db
                log.info("🐢 : deleting all ProcessInfo2")
                // processInfo2Repo.deleteAll()
                this@PsController.processList.clear()

                log.info("🐢 : saving ProcessInfo2")
                for (i in rpl.processesList) {
                    // pb -> java
                    val p = Dt.ProcessInfo2.convertFromPb(i)
                    try {
                        // log.info("🐢 : saving ProcessInfo2: {}{}{}",S.BLUE, p.toString(), S.NOR);
                        // processInfo2Repo.save(p)
                        this@PsController.processList.add(p)
                    } catch (e: Exception) {
                        log.error("🐢 : {}Error saving ProcessInfo2:{} {}{}", S.RED, p.toString(), e.message, S.NOR)
                    }
                }

                log.info(S.GREEN + "🌐️ : for-loop done saving ProcessInfo2" + S.NOR)
            }
        } catch (e: StatusException) {
            log.error("🐢 : {}Error getting ProcessInfo: {}{}", S.RED, e.message, S.NOR)
            return
        } catch (e: Exception) {
            log.error(
                "🐢 Error happended while updating ProcessInfo2 : {}{}: {}{}", S.RED, e,
                e.message,
                S.NOR
            )
            e.printStackTrace()
        }

        // 2.
        // 🦜 : check the registers

        // 2.1 check mustRun
        val mustRunList = processRegisterRepo.findByType("must")

        for (r in mustRunList) {
            /*
             if hash == "", count by exeName only, otherwise, check both exeName and hash
             */
            var count = 0L

            /*
             // prepare the spec
             var spec = Specification.where<Dt.ProcessInfo2?>(null)
             ....
             count = processInfo2Repo.count(spec)
             */

            // [2024-02-29]🦜 : Here we use the processList instead of the db
            var l = this.processList.filter { it.name == r.exeName }
            if (r.hash.isNotEmpty()) {
                l = l.filter { it.hash == r.hash }
            }
            if (r.userName.isNotEmpty()) {
                l = l.filter { it.user == r.userName }
            }
            count = l.size.toLong()

            if (count == 0L) {
                // 🦜 : add a failure record
                val f = Dt.ProcessRegisterFailure()
                f.exeName = r.exeName
                f.reason = "关键进程不存在"
                f.type = "must"
                processRegisterFailureRepo.save(f)
            }
        }

        // 2.2 check okRun
        /*
         🦜 : Okay... or simplicity, we just check the hash of ok-process for now...

         for each process, if there's isn't a record of this ExeName, then throw a failure record

         <2024-03-04 Mon>: Update, here we check through the toBeProcessedOneoffProcessInfo
         */
        processOkFailure()
    }

    /* 🦜 : Here we calculate the `ok` failure register. We work through the
       `toBeProcessedOneoffProcessInfo` list and throw a failure record if the
       process is not in the register */

    fun processOkFailure(){
        val maxPerBatch = 1000
        for (i in 0 until maxPerBatch) {
            val p = toBeProcessedOneoffProcessInfo.poll()
            if (p == null) {
                break
            }

            // prepare the spec
            var spec = Specification.where<Dt.ProcessRegister?>(null)

            // 🦜 : Hash check not supported for OK-process 
            // if (p.hash.isNotEmpty()) {
            //     spec = spec.and(Dt.ProcessRegisterBase_.hashEquals(p.hash))
            // }
            spec = spec.and(Dt.ProcessRegisterBase_.exeNameEquals(p.comm))

            this.userService.syncIfTooOld()

            // 🦜 : 1. Skip check if uid = 0 (root)
            if (p.uid == 0) {
                continue
            }

            // 🦜 : 2. also skip check if there's a register with this `exeName` and an empty `userName`
            val spec0 = spec.and(Dt.ProcessRegisterBase_.userNameEquals(""))
            if (processRegisterRepo.count(spec0) > 0) {
                continue
            }

            // 🦜 : 3. check the user
            val who: Dt.UserInfo2? = this.userService.cachedUsers[p.uid]
            who?.let {
                spec = spec.and(Dt.ProcessRegisterBase_.userNameEquals(it.name))
            }

            val count = processRegisterRepo.count(spec)
            if (count == 0L) {  //not found
                // 🦜 : add a failure record

                val f = Dt.ProcessRegisterFailure(
                    //hash not supported yet, 🦜 : and probably won't
                    timeOfDiscovery = Date(),
                    fullCommandLine = p.argv
                )

                f.reason = "进程不在注册表中"
                f.type = "ok"
                f.userName = who?.name ?: ""
                f.exeName = p.comm
                f.hash = ""

                processRegisterFailureRepo.save(f)
            }
        }
}

    @Operation(summary = "取消进程注册")
    @GetMapping("/unregister")
    fun unregister(id: Long): Dt.Error {
        // 🦜 : delete the process
        processRegisterRepo.deleteById(id)
        return Common.ok()
    }

    @Operation(
        summary = "注册进程",
        description = "注册一个进程，这个进程可以是一个关键进程，也可以是一个可以跑的进程,📗️ :`id`,`createTime`是自动填的所以不用传,`type`填`ok`(可以跑的应用)或`must`（必须跑的应用）。"
    )
    @PostMapping("/register")
    fun register(@RequestBody r: Dt.ProcessRegister): Dt.Error {
        // 🦜 : save the process
        log.info("🐢 : registering: {}", r.exeName)
        try {
            r.createTime = Date()
            processRegisterRepo.save(r)
            return Common.ok()
        } catch (e: Exception) {
            log.error("🐢 : {}Error registering: {}{}", S.RED, e.message, S.NOR)
            return Common.bad(e.message)
        }
    }

    @Operation(
        summary = "获取进程异常信息",
        description = "获取进程注册信息,返回*用户名*，*应用名*，*应用哈希*，*种类（关键应用|注册应用）*,*原因*，*id*，*错误发现时间*"
    )
    @GetMapping("/getFailures")
    @Synchronized
    fun getFailures(
        pageNum: Int, pageSize: Int,
        @RequestParam(required = false) @Parameter(
            description = "找哪种类型的记录",
            examples = [ExampleObject(
                            name = "must",
                            value = "must",
                            description = "关键应用注册记录，必须跑的应用"
                        ), ExampleObject(name = "ok", value = "ok", description = "应用注册记录，可以跑的应用")]
        ) type: String?,

        @RequestParam(required = false) @Parameter(
            description = "按应用名查找",
            examples = [ExampleObject(name = "systemd", value = "systemd", description = "查找应用名含systemd的记录")]
        ) exeName: String?,

        @RequestParam(required = false) @Parameter(
            description = "按哪行排序",
            required = false,
            examples = [ExampleObject(
                            name = "timeOfDiscovery",
                            value = "timeOfDiscovery",
                            description = "默认按发现时间排序"
                        ), ExampleObject(name = "exeName", value = "exeName", description = "按进程名排序")]
        ) sortBy: String?,

        @RequestParam(required = false) @Parameter(description = "是否升序", required = false) asc: Boolean?
    ): W<Dt.Page<Dt.ProcessRegisterFailure>?> {
        syncIfTooOld()
        // 1. prepare the pageable
        val sortBys: Set<String> = setOf("id", "exeName", "timeOfDiscovery")
        val pageable = checkThingsAndPreparePageable(pageNum, pageSize, sortBys, sortBy, asc, "timeOfDiscovery")

        // 2. prepare the spec
        var spec = Specification.where<Dt.ProcessRegisterFailure>(null)
        if (!type.isNullOrEmpty()) {
            spec = spec.and(Dt.ProcessRegisterBase_.typeEquals_f(type))
        }

        if (!exeName.isNullOrEmpty()) {
            spec = spec.and(Dt.ProcessRegisterBase_.exeNameContains_f(exeName))
        }

        // 3. query
        val page = processRegisterFailureRepo.findAll(spec, pageable)

        return W(0, Dt.Page(page.content, page.totalElements))
    }

    @Operation(
        summary = "获取进程注册信息",
        description = "获取进程注册信息,返回*用户名*，*应用名*，*应用哈希*，*种类（关键应用|注册应用）*,*原因*，*id*，*创建时间*"
    )
    @GetMapping("/getRegisters")
    fun getRegisters(
        pageNum: Int, pageSize: Int,
        @RequestParam(required = false) @Parameter(
            description = "找哪种类型的记录",
            examples = [ExampleObject(name = "must", value = "must", description = "关键应用注册记录，必须跑的应用"),
                        ExampleObject(name = "ok", value = "ok", description = "应用注册记录，可以跑的应用")]
        ) type: String?,

        @RequestParam(required = false) @Parameter(
            description = "按应用名查找",
            examples = [ExampleObject(name = "systemd", value = "systemd", description = "查找应用名含systemd的记录")]
        ) exeName: String?,
        @RequestParam(required = false) @Parameter(
            description = "按哪行排序",
            required = false,
            examples = [ExampleObject(name = "exeName", value = "exeName", description = "默认按进程名排序"),
                        ExampleObject(name = "id", value = "id", description = "按id排序")]
        ) sortBy: String?,
        @RequestParam(required = false) @Parameter(description = "是否升序", required = false) asc: Boolean?
    ): W<Dt.Page<Dt.ProcessRegister>?> {
        // 1. prepare the pageable
        val sortBys: Set<String> = setOf("id", "exeName")
        val pageable = checkThingsAndPreparePageable(pageNum, pageSize, sortBys, sortBy, asc, "exeName")

        // 2. prepare the spec
        var spec = Specification.where<Dt.ProcessRegister>(null)
        if (!type.isNullOrEmpty()) {
            spec = spec.and(Dt.ProcessRegisterBase_.typeEquals(type))
        }

        if (!exeName.isNullOrEmpty()) {
            spec = spec.and(Dt.ProcessRegisterBase_.exeNameContains(exeName))
        }

        try {
            // 3. query
            val page = processRegisterRepo.findAll(spec, pageable)
            return W(0, Dt.Page(page.content, page.totalElements))
        } catch (e: Exception) {
            log.error("🐢 : {}Error getting Register: {}{}", S.RED, e.message, S.NOR)
            return W(1, null, e.message ?: "未知错误: null message")
        }

    }

    companion object {
        /*
         🦜 :
         This controller holds three dbs:
         1. process-db (transient)
         2. failure-db (transient)
         3. records-db (persistent)

         This controller's gonna do most of the heavy work in its `sync()`
         method. Most importantly, it's gonna

         1. get all the process info from agent and cache that in the database
         2. check the registers

         There're are two types of registers: `must-run` and `ok-run`

         2.1. `must-run` means the process must be running, if it's not running, we
         throw a failure record in the database

         2.2. `ok-run` means the process can be running, checking this is a bit more elaborate:

         we need to:

         for each process:
         we add a failure record if it's neither an `ok-run` nor a `must-run`

         Every time we sync(), we clear the process-db and failure-db first.

         🐢 : So only the `records-db` is a 'real' persistance db editable by the
         front-end client.

         🦜 : Yeah.
         */
        const val CounterKey: String = "process-sync"
    }
} // class PsController

internal interface GenericCounterRepo : JpaRepository<Dt.GenericCounter, Long>,
                                        JpaSpecificationExecutor<Dt.GenericCounter> {
    fun countByNameAndCreateTimeBetween(name: String, since: Date, until: Date): Long
}

@Component
internal class CounterService {
    @Autowired
    lateinit var repo: GenericCounterRepo

    fun inc(name: String) {
        repo.save(Dt.GenericCounter.create(name))
    }

    @Synchronized
    fun get(n: String, since: Date, until: Date): Long {
            return repo.countByNameAndCreateTimeBetween(n, since, until)
        }

    @Synchronized
    fun getInNDaysFromNow(n: String, d: Int): Long {
        // 🦜 : get today's (d = 0), yesterday's (d = 1), the day before yesterday's (d = 2), ...
        val di = DayInterval.nDaysFromNow(d)
        return this.get(n, di.since, di.until)
    }
}

internal class DayInterval(var since: Date, var until: Date) {
    val sinceString: String
        get() = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(since)

    val untilString: String
        get() = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(until)

    companion object {
        fun nDaysFromNow(d: Int): DayInterval {
            val c = Calendar.getInstance() // now
            c.add(Calendar.DATE, -d)
            // set hh:mm:ss to 00:00:00
            c[Calendar.HOUR_OF_DAY] = 0
            c[Calendar.MINUTE] = 0
            c[Calendar.SECOND] = 0

            val since = c.time
            c.add(Calendar.DATE, 1)
            val until = c.time

            return DayInterval(since, until)
        }
    }
}

// --------------------------------------------------
interface ChangePswdRecordRepo : JpaRepository<Dt.ChangePswdRecord, Long>

@Tag(
    name = "root用户密码管理",
    description = "🦜: `root`用户的密码会不停的变，用这个服务来查看现在最新（以及以往）`root`的登录密码吧。"
)
@RestController
@RequestMapping("/rootPswd")
@EnableScheduling
internal class RootAccountController : ControllerBase() {
    @Autowired
    lateinit var repo: ChangePswdRecordRepo

    @PostMapping("/set")
    @Operation(summary = "设置root用户的密码")
    fun set(@RequestBody o: Dt.ChangePswdReq): Dt.Error {
            val ca = Calendar.getInstance() // the date factory
            val now = ca.time

            // upcast to Dt.ChangePswdRecord
            val r = Dt.ChangePswdRecord()

            r.pswd = o.pswd
            r.expiryTime = o.expiryTime
            r.createTime = now

            try {
                this.saveAndApply(r)
            } catch (e: StatusException) {
                val msg = e.status.description // can be null
                log.error(S.RED + "📕️ RPC failed: {}" + S.NOR, e.status)
                return Common.bad(msg)
            }

            return Common.ok()
        }

    @GetMapping("/get")
    @Operation(summary = "获取root用户的密码记录")
    fun get(pageNum: Int?, pageSize: Int?): W<Dt.Page<Dt.ChangePswdRecord>?> {
            // return rootAccountService.getRegisRecord(pageNum,pageSize);
            if (pageNum == null || pageSize == null) {
                return W(-1, null, "pageNum or pageSize is null")
            }
            return getPage(repo, pageNum, pageSize)
        }

    /*
     @Scheduled(cron = "0 0/10 * * * ?")
     @Transactional
     /**
     * @brief Try to change the root password to a random one
     *
     * 🐢 : This method is scheduled to run every 10 minutes. It generate a
     * Dt.Dt.ChangePswdRecord with `createTime` = now(), `expiryTime` = now() +
     * 9min30sec;
     *
     * 🦜 : Will this be run immediately?
     *
     * 🐢 : No, it will be run at the next 10 minute mark. (good for unit test)
     */
     @Synchronized
     fun changeRootPswdMaybe() {
     // 0. if it's time to change root password
     val c = Calendar.getInstance() // the date factory
     val now = c.time

     if (now.before(noUpdateBefore)) {
     log.info("🐸 : not time to change root password yet")
     return
     }

     try {
     // 1. generate a Dt.Dt.ChangePswdRecord
     val nwPasswd = PasswdUtil.generateSecurePassword(16)
     val r = Dt.ChangePswdRecord()
     r.pswd = nwPasswd
     r.createTime = now
     r.expiryTime = Date(now.time + (9 * 60 + 30) * 1000) // 9min30sec latter
     this.saveAndApply(r)
     } catch (e: StatusRuntimeException) {
     val msg = e.status.description
     log.error(S.RED + "📕️ RPC failed: {}" + S.NOR, e.status)
     }
     }
     */

    /**
     * @brief save the record and set the `noUpdateBefore` to the expiry time
     * @param r the record to save and apply
     */
    @Throws(StatusException::class)
    fun saveAndApply(r: Dt.ChangePswdRecord) {
        if (r.expiryTime!!.before(Date())) {
            log.error("⚠️ :? trying to apply old Dt.ChangePswdRecord")
            return
        }
        repo.save(r)
        noUpdateBefore = r.expiryTime
        this.askAgentToChangePswd(r.pswd)
    }

    @Throws(StatusException::class)
    fun askAgentToChangePswd(pswd: String) {
        // return rootAccountService.askAgentToChangePswd(o);
        // val b = HelloRequest.newBuilder().setName(pswd).build()
        val b = pb_helloRequest { this.name = pswd }
        log.info(S.GREEN + "📕️ Asking agent to change pswd to : {}" + S.NOR, pswd)
        runBlocking {
            val r = c.stub.changeRootPswd(b)
            log.info(S.GREEN + "📕️ Agent replied : {}" + S.NOR, r.msg)
        }
    }

    internal object PasswdUtil {
        private const val LOWERCASE_CHARACTERS = "abcdefghijklmnopqrstuvwxyz"
        private const val UPPERCASE_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        private const val NUMERIC_CHARACTERS = "0123456789"

        //    private static final String SPECIAL_CHARACTERS = "!@#$%^&*()-_=+[]{}|;:,.<>/?";
        private const val ALL_CHARACTERS = LOWERCASE_CHARACTERS + UPPERCASE_CHARACTERS + NUMERIC_CHARACTERS

        fun generateSecurePassword(length: Int): String {
            val password = StringBuilder()

            val random: Random = SecureRandom()

            // Ensure at least one character from each character set
            password.append(LOWERCASE_CHARACTERS[random.nextInt(LOWERCASE_CHARACTERS.length)])
            password.append(UPPERCASE_CHARACTERS[random.nextInt(UPPERCASE_CHARACTERS.length)])
            password.append(NUMERIC_CHARACTERS[random.nextInt(NUMERIC_CHARACTERS.length)])

            // Fill the rest of the password
            for (i in 3 until length) {
                val randomIndex = random.nextInt(ALL_CHARACTERS.length)
                password.append(ALL_CHARACTERS[randomIndex])
            }

            // Shuffle the characters in the password for better randomness
            val passwordArray = password.toString().toCharArray()
            for (i in passwordArray.size - 1 downTo 1) {
                val index = random.nextInt(i + 1)
                val temp = passwordArray[index]
                passwordArray[index] = passwordArray[i]
                passwordArray[i] = temp
            }

            return String(passwordArray)
        }
    }

    companion object {
        // --------------------------------------------------
        // one for whole class
        var noUpdateBefore: Date? = Date(0) // 1970-01-01 00:00:00
    }
}                               //class RootAccountController


// ^^^ controllers
// --------------------------------------------------
object S {
    const val RED: String = "\u001B[31m"
    const val GREEN: String = "\u001B[32m"
    const val YELLOW: String = "\u001B[33m"
    const val BLUE: String = "\u001B[34m"
    const val MAGENTA: String = "\u001B[35m"
    const val CYAN: String = "\u001B[36m"
    const val NOR: String = "\u001B[0m"
}

// common stuff
class Common(agentUrl: String) : AutoCloseable {
    val json: ObjectMapper = mapper()
    val ch = ManagedChannelBuilder.forTarget(agentUrl).usePlaintext().build()
    val stub = GreeterCoroutineStub(ch)

    // dtor (finalizer)
    override fun close() {
        try {
            // ch.shutdownNow().awaitTermination(5, TimeUnit.SECONDS)
        } catch (e: InterruptedException) {
            log.error("❌️ : error closing grpc channel: ", e)
        }
    }

    companion object {
        var log: Logger = LoggerFactory.getLogger(Common::class.java)

        fun ok(): Dt.Error {
            // return json.writeValueAsString(Dt.Error.SUCCESS);
            return Dt.Error.SUCCESS
        }

        // fun bad(msg: String?, status: HttpStatus = HttpStatus.OK): Dt.Error {
        // 🦜 : Previously we can change the status code, but we have given that up..
        fun bad(msg: String?): Dt.Error {
            return Dt.Error(-1, msg ?: "unknown error (null msg)")
        }

        fun mapper(): ObjectMapper {
            val m = ObjectMapper()

            val df = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
            m.setDateFormat(df)
            return m
        }
    }
}

@OpenAPIDefinition(
    info = Info(
        title = "这是可信服务器（受控服务器）管理系统的API",
        version = "1.2",
        description = "⚠️: 请注意所有时间戳的格式都是`yyyy-MM-dd hh:mm:ss`，" +
            "但是因为作者最终还是没能搞懂swagger的对象反映射魔法，所以在这个swagger自动生成的" +
            "文档里示例的时间戳格式一般会是一串像`2023-12-25T09:36:22.847Z`的东西，" +
            "请在发送和接受参数的时候使用 `yyyy-MM-dd hh:mm:ss` 格式。"
    )
)
@SpringBootApplication
@EnableWebSocket
class M : WebSocketConfigurer {
    /*
     🦜 : bean for repo is automatically created.
     */
    // @JvmField
    // @Autowired
    // lateinit var openPortRepo: OpenPortRepo

    @Autowired
    lateinit var safeboxRecordRepo: SafeboxRecordRepo

    @Autowired
    lateinit var changePswdRecordRepo: ChangePswdRecordRepo

    @Autowired
    lateinit var safeboxFileRepo: SafeboxFileRepo


    @Value("\${machine.agent-url}")
    lateinit var agentUrl: String


    @Bean
    fun common(): Common {
        // --------------------------------------------------
        /*

         🦜 : Here we start the scheduled tasks (in fact there're many places
         we can do this in, but let's just do it here)
         */
        t.scheduleAtFixedRate({
                                   try {
                                       // Wc1.say()
                                       // Wc4.say()

                                       // 🦜 : say them

                                       // Dt.GetResourceInfoRpl2 r = basicController.askResourceInfoFromAgent();
                                       // Wc1.say(json.writeValueAsString(r));

                                   } catch (e: Exception) {
                                       log.error("🐸 : error saying cctv1", e)
                                   }
                               },
                               Duration.of(5, ChronoUnit.SECONDS)
        ) // every two seconds, boardcast to all listener of /hi

        return Common(agentUrl)
    }

    @Autowired
    lateinit var t: ThreadPoolTaskScheduler

    @Autowired
    lateinit var wc1: Wc1 // 🦜 : why they're here? 🐢 : Brought in for register

    @Autowired
    lateinit var wc4: Wc4


    /*
     🦜 : Kinda kave to do it this way, so we ensure that there's only one wc1 and wc4
     */
    override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) {
        registry.addHandler(wc1, "/ws/cctv1").setAllowedOrigins("*")
        registry.addHandler(wc4, "/ws/cctv4").setAllowedOrigins("*")
    }

    companion object {
        var log: Logger = LoggerFactory.getLogger(M::class.java)

        @JvmStatic
        fun main(args: Array<String>) {
            SpringApplication.run(M::class.java, *args)
        }
    }
}


@Configuration
internal class C1 {
    @Bean
    fun wc1(): Wc1  = Wc1()

    @Bean
    fun wc4(): Wc4 = Wc4()

    /*
     🦜 : By declaring a primary bean of ObjectMapper, we change how spring-boot parse and serialize json
     */
    @Primary
    @Bean
    fun json(): ObjectMapper = Common.mapper()

    @Bean
    fun threadPoolTaskScheduler(): ThreadPoolTaskScheduler {
        var t = ThreadPoolTaskScheduler()
        t.poolSize = 5
        return t
    }
}

abstract class Wc : TextWebSocketHandler() {
    @Throws(IOException::class)
    override fun afterConnectionEstablished(session: WebSocketSession) {
        log.info("🌱 established: $session")
        // session.sendMessage(new TextMessage("🌐️ 欢迎收看: " + channel()));
        // add to listener
        if (all[channel()] == null) {
            all[channel()] = CopyOnWriteArraySet()
        }
        all[channel()]!!.add(session)
    }


    @Throws(IOException::class, RuntimeException::class)
    override fun afterConnectionClosed(session: WebSocketSession, status: CloseStatus) {
        log.info("🚮️ closed: $session")
        // remove from listener
        if (all[channel()] != null) {
            all[channel()]!!.remove(session)
        } else {
            // ?? fatal, unregisted channel?
            throw RuntimeException("fatal, unregisted channel?")
        }
    }

    @Throws(IOException::class)
    public override fun handleTextMessage(session: WebSocketSession, message: TextMessage) {
        log.info("🐸 : [ignored]: audience said " + message.payload)
        // session.sendMessage(new TextMessage("Hi from server"));
    }

    abstract fun channel(): String

    companion object {
        var log: Logger = LoggerFactory.getLogger(Wc::class.java)

        var all: ConcurrentHashMap<String, CopyOnWriteArraySet<WebSocketSession>?> = ConcurrentHashMap()

        fun boardcast(k: String, msg: String) {

            var msg = msg
            val mmsg = msg
            // cap to 30 char is msg is too long
            if (msg.length > 30) {
                msg = msg.substring(0, 30) + "..."
            }

            log.info("🐸 : boardcast() to " + k + ": " + S.CYAN + msg + S.NOR)
            if (all[k] != null) {
                for (s in all[k]!!) {
                    try {
                        s.sendMessage(TextMessage(mmsg))
                    } catch (e: Exception) {
                        log.error("🐸 : error sending msg: ", e)
                    }
                }
            }

        }
    }
} // class Wc

class Wc1 : Wc() {
    override fun channel(): String  = "/cctv1"

    companion object {
        fun say() {
            boardcast("/cctv1", "CCTV1: " + LocalDateTime.now().toString())
        }

        fun say(s: String) {
            boardcast("/cctv1", s)
        }
    }
}

class Wc4 : Wc() {
    override fun channel(): String  = "/cctv4"

    companion object {
        var words: Array<String> = arrayOf("🐸", "🐸🐸", "🐸🐸🐸", "🐸🐸🐸🐸", "🐸🐸🐸🐸🐸")
        var i: Int = 0
        fun say() {
            boardcast("/cctv4", "CCTV4: " + words[i++ % words.size])
        }

        fun say(s: String) {
            boardcast("/cctv4", s)
        }
    }
}

/*
  🦜 : Here, the globle exception handler is defined
*/
@ControllerAdvice
internal class GlobalExceptionHandler {
    @ExceptionHandler(RuntimeException::class, IllegalArgumentException::class)
    fun handleException(e: Exception): ResponseEntity<Dt.Error> {
        // Customize your response here
        // return new ResponseEntity<>("An error occurred: " + e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
        // println("🐢 handler called: " + e.message)
        // return Dt.Error(-1, e.message ?: "unknown error (null msg)")
        return ResponseEntity(Dt.Error(-1, e.message ?: "unknown error (null msg)"), HttpStatus.OK)
    }
}
